/*
 Copyright 2020 Adobe. All rights reserved.
 This file is licensed to you under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License. You may obtain a copy
 of the License at http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software distributed under
 the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
 OF ANY KIND, either express or implied. See the License for the specific language
 governing permissions and limitations under the License.
 */

import AEPCore
import Foundation

/// Represents the JSON structure for the company context
private struct CompanyContext: Codable {
    let namespace: String
    let marketingCloudId: String

    init(marketingCloudId: String) {
        self.namespace = "imsOrgID"
        self.marketingCloudId = marketingCloudId
    }
}

/// Represents the JSON structure for a list of `UserID`
private struct Users: Codable {
    let userIDs: [UserID]
}

/// Represents a user id with a namespace, value, and type
private struct UserID: Codable {
    let namespace: String
    let value: String
    let type: String
}

/// Responsible for reading the shared state of multiple extensions to read the identifiers
struct MobileIdentities: Codable {
    typealias SharedStateProvider = (String, Event?) -> SharedStateResult?
    private var companyContexts: [CompanyContext]?
    private var users: [Users]?

    static let NAMESPACE_MCID = "4"
    static let INTEGRATION_CODE = "integrationCode"
    static let NAMESPACE_ID = "namespaceId"
    static let MCPNS_DPID = "20920"
    static let IDFA_DSID = "DSID_20915"
    static let NAMESPACE_ANALYTICS_AID = "AVID"
    static let NAMESPACE_USERIDENTIFIER = "vid"
    static let NAMESPACE_AUDIENCE_UUID = "0"
    static let NAMESPACE_TARGET_TNT_ID = "tntid"
    static let NAMESPACE_TARGET_THIRD_PARTY_ID = "3rdpartyid"
    static let ANALYTICS = "analytics"
    static let TARGET = "target"

    /// Collects all the identities from various extensions and packages them into a JSON string
    /// - Parameters:
    ///   - event: the `Event` generated by the GetSdkIdentities API
    ///   - sharedStateProvider: a function that can resolve `SharedState` given an extension name
    /// - Returns: a JSON formatted string with all the identities from various extensions
    mutating func collectIdentifiers(event: Event, sharedStateProvider: SharedStateProvider) {
        if let companyContexts = getCompanyContexts(event: event, sharedStateProvider: sharedStateProvider) {
            self.companyContexts = [companyContexts]
        }

        var userIds = [UserID]()
        userIds.append(contentsOf: getVisitorIdentifiers(event: event, sharedStateProvider: sharedStateProvider))
        userIds.append(contentsOf: getAnalyticsIdentifiers(event: event, sharedStateProvider: sharedStateProvider))
        userIds.append(contentsOf: getAudienceIdentifiers(event: event, sharedStateProvider: sharedStateProvider))
        userIds.append(contentsOf: getTargetIdentifiers(event: event, sharedStateProvider: sharedStateProvider))

        if !userIds.isEmpty {
            users = [Users(userIDs: userIds)]
        }
    }

    /// Determines if all the shared states required to collect identities are ready
    /// - Parameters:
    ///   - event: the `Event` generated by the GetSdkIdentities API
    ///   - sharedStateProvider: a function that can resolve `SharedState` given an extension name
    /// - Returns: True if all shared states are ready, false otherwise
    func areSharedStatesReady(event: Event, sharedStateProvider: SharedStateProvider) -> Bool {
        let identityStatus = sharedStateProvider(IdentityConstants.EXTENSION_NAME, event)?.status ?? .none
        let configurationStatus = sharedStateProvider(IdentityConstants.SharedStateKeys.CONFIGURATION, event)?.status ?? .none
        let analyticsStatus = sharedStateProvider(IdentityConstants.SharedStateKeys.ANALYTICS, event)?.status ?? .none
        let audienceStatus = sharedStateProvider(IdentityConstants.SharedStateKeys.AUDIENCE, event)?.status ?? .none
        let targetStatus = sharedStateProvider(IdentityConstants.SharedStateKeys.TARGET, event)?.status ?? .none

        return identityStatus != .pending &&
            configurationStatus != .pending &&
            analyticsStatus != .pending &&
            audienceStatus != .pending &&
            targetStatus != .pending
    }

    // MARK: Private APIs

    /// Gets all the required identity from the Identity extension
    /// - Parameters:
    ///   - event: the `Event` generated by the GetSdkIdentities API
    ///   - sharedStateProvider: a function that can resolve `SharedState` given an extension name
    /// - Returns: a list of all the Identity extension identities in the form of a `UserID`
    private func getVisitorIdentifiers(event: Event, sharedStateProvider: SharedStateProvider) -> [UserID] {
        guard let identitySharedState = sharedStateProvider(IdentityConstants.EXTENSION_NAME, event) else { return [] }

        var visitorIds = [UserID]()

        // marketing cloud id
        if let marketingCloudId = identitySharedState.value?[IdentityConstants.EventDataKeys.VISITOR_ID_ECID] as? String {
            visitorIds.append(UserID(namespace: MobileIdentities.NAMESPACE_MCID, value: marketingCloudId, type: MobileIdentities.NAMESPACE_ID))
        }

        // visitor ids and advertising id
        // Identity sets the advertising identifier both in ‘visitoridslist’ and as ‘advertisingidentifer’ in the Identity shared state.
        // So, there is no need to fetch the advertising identifier with advertisingidentifer namespace DSID_20914 separately.
        if let customVisitorIds = identitySharedState.value?[IdentityConstants.EventDataKeys.VISITOR_IDS_LIST] as? [[String: Any]] {
            // convert each `CustomIdentity` dictionary representations to a `UserID`, then remove any nil values
            visitorIds.append(contentsOf: customVisitorIds.map { MobileIdentities.dictToUserID(dict: $0) }.compactMap { $0 })
        }

        // push identifier
        if let pushId = identitySharedState.value?[IdentityConstants.EventDataKeys.PUSH_IDENTIFIER] as? String, !pushId.isEmpty {
            visitorIds.append(UserID(namespace: MobileIdentities.MCPNS_DPID, value: pushId, type: MobileIdentities.INTEGRATION_CODE))
        }

        return visitorIds
    }

    /// Gets all the required identity from the Analytics extension
    /// - Parameters:
    ///   - event: the `Event` generated by the GetSdkIdentities API
    ///   - sharedStateProvider: a function that can resolve `SharedState` given an extension name
    /// - Returns: a list of all the Analytics extension identities in the form of a `UserID`
    private func getAnalyticsIdentifiers(event: Event, sharedStateProvider: SharedStateProvider) -> [UserID] {
        guard let analyticsSharedState = sharedStateProvider(IdentityConstants.SharedStateKeys.ANALYTICS, event) else { return [] }

        var analyticsIds = [UserID]()

        if let aid = analyticsSharedState.value?[IdentityConstants.Analytics.ANALYTICS_ID] as? String {
            analyticsIds.append(UserID(namespace: MobileIdentities.NAMESPACE_ANALYTICS_AID, value: aid, type: MobileIdentities.INTEGRATION_CODE))
        }

        if let vid = analyticsSharedState.value?[IdentityConstants.Analytics.VISITOR_IDENTIFIER] as? String {
            analyticsIds.append(UserID(namespace: MobileIdentities.NAMESPACE_USERIDENTIFIER, value: vid, type: MobileIdentities.ANALYTICS))
        }

        return analyticsIds

    }

    /// Gets all the required identity from the Audience extension
    /// - Parameters:
    ///   - event: the `Event` generated by the GetSdkIdentities API
    ///   - sharedStateProvider: a function that can resolve `SharedState` given an extension name
    /// - Returns: a list of all the Audience extension identities in the form of a `UserID`
    private func getAudienceIdentifiers(event: Event, sharedStateProvider: SharedStateProvider) -> [UserID] {
        guard let audienceSharedState = sharedStateProvider(IdentityConstants.SharedStateKeys.AUDIENCE, event) else { return [] }

        var audienceIds = [UserID]()

        if let dpuuid = audienceSharedState.value?[IdentityConstants.Audience.DPUUID] as? String {
            let dpid = audienceSharedState.value?[IdentityConstants.Audience.DPID] as? String ?? ""
            audienceIds.append(UserID(namespace: dpid, value: dpuuid, type: MobileIdentities.NAMESPACE_ID))
        }

        if let uuid = audienceSharedState.value?[IdentityConstants.Audience.UUID] as? String {
            audienceIds.append(UserID(namespace: MobileIdentities.NAMESPACE_AUDIENCE_UUID, value: uuid, type: MobileIdentities.NAMESPACE_ID))
        }

        return audienceIds
    }

    /// Gets all the required identities from the Target extension
    /// - Parameters:
    ///   - event: the `Event` generated by the GetSdkIdentities API
    ///   - sharedStateProvider: a function that can resolve `SharedState` given an extension name
    /// - Returns: a list of all the Target extension identities in the form of a `UserID`
    private func getTargetIdentifiers(event: Event, sharedStateProvider: SharedStateProvider) -> [UserID] {
        guard let targetSharedState = sharedStateProvider(IdentityConstants.SharedStateKeys.TARGET, event) else { return [] }

        var targetIds = [UserID]()

        if let tntId = targetSharedState.value?[IdentityConstants.Target.TNT_ID] as? String {
            targetIds.append(UserID(namespace: MobileIdentities.NAMESPACE_TARGET_TNT_ID, value: tntId, type: MobileIdentities.TARGET))
        }

        if let thirdPartyId = targetSharedState.value?[IdentityConstants.Target.THIRD_PARTY_ID] as? String {
            targetIds.append(UserID(namespace: MobileIdentities.NAMESPACE_TARGET_THIRD_PARTY_ID, value: thirdPartyId, type: MobileIdentities.TARGET))
        }

        return targetIds
    }

    /// Gets all the required identity from the Configuration extension
    /// - Parameters:
    ///   - event: the `Event` generated by the GetSdkIdentities API
    ///   - sharedStateProvider: a function that can resolve `SharedState` given an extension name
    /// - Returns: a list of all the Configuration extension identities in the form of a `CompanyContext`
    private func getCompanyContexts(event: Event, sharedStateProvider: SharedStateProvider) -> CompanyContext? {
        guard let configurationSharedState = sharedStateProvider(IdentityConstants.SharedStateKeys.CONFIGURATION, event) else { return nil }

        guard let marketingCloudOrgId = configurationSharedState.value?[IdentityConstants.Configuration.EXPERIENCE_CLOUD_ORGID] as? String, !marketingCloudOrgId.isEmpty else { return nil }

        return CompanyContext(marketingCloudId: marketingCloudOrgId)
    }

    /// Converts a `CustomIdentity` dictionary representation into a `UserID`
    /// - Returns: a `UserID` where the namespace is value, value is identifier, and type is integrationCode
    private static func dictToUserID(dict: [String: Any]) -> UserID? {
        guard let type = dict[CustomIdentity.CodingKeys.type.rawValue] as? String,
              let identifier = dict[CustomIdentity.CodingKeys.identifier.rawValue] as? String,
              !identifier.isEmpty else { return nil }
        return UserID(namespace: type, value: identifier, type: MobileIdentities.INTEGRATION_CODE)
    }
}
